iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0
Software Development

Django 2024: 從入門到SaaS實戰系列 第 5

Django in 2024: 淺嚐Model, Url與Template

  • 分享至 

  • xImage
  •  

我們今天就來繼續更深入Django的MTV架構,看看有哪些細節是我們需要再注意的

以下程式碼一樣:https://github.com/class83108/django_project/tree/hello_world

今日的重點:

  • Url:
    • 如何在路由中配置變數
    • 如何在Template與View中更簡潔的調用路由-路由的命名
  • Template:
    • Template中的內置標籤
    • 讓Template的應用更上一層樓:extends、include與組件化
  • Model:
    • Model中欄位其他常用配置
    • View中的ORM語法

Url

Url中配置變數

路由可以配置變數,而配置變數的種類以下幾種:

  • str:匹配非空字符串,但是不包含“/”,在預設狀況直接套用
  • int:匹配大於等於0的正整數
  • slug:可以匹配任何ASCII字符串、數字與下劃線還有連接符,通常用意是更方便使用者知道這個url是通往哪裡
  • uuid:匹配UUID的格式,但是為了避免引起衝突,必須用全小寫以及需要包含”-”。會返回一個UUID的實例
  • path:相比於str更加自由,除了空字符串一樣無法匹配之外,也能匹配“/”
# 根目錄的urls.py
from .views import index_view

urlpatterns = [
    ....
    path("<str:user_name>/<int:age>/<slug:page_name>/<uuid:user_id>", index_view),
]

# 在根目錄建立views.py
from django.shortcuts import render

def index_view(request, user_name, age, page_name, user_id):
# 需要在視圖的參數中直接寫上變數名稱,跟get請求中參數不同,不要搞混

    url_info = {
        "user_name": user_name,
        "user_name_type": type(user_name).__name__,
        "age": age,
        "age_type": type(age).__name__,
        "page_name": page_name,
        "page_name_type": type(page_name).__name__,
        "user_id": user_id,
        "user_id_type": type(user_id).__name__,
    }

    return render(request, "index.html", {"url_info": url_info})
    
# 在templates目錄下建立index.html
<body>
	<div>
		{% for key, value in url_info.items %}
			<p>{{ key }}: {{ value }}</p>
		{% endfor %}
	</div>
</body>

然後我們在瀏覽器就可以看到
https://ithelp.ithome.com.tw/upload/images/20240916/201618669tXsJLms5c.png

那path的部分

# 根目錄下的urls.py
from .views import index_view, demo_path_view

urlpatterns = [
		...
    path("<path:to_inde_page>", demo_path_view),
]

# 根目錄下的views.py
def demo_path_view(request, to_inde_page):

    return render(
        request,
        "index.html",
        {
            "to_inde_page": to_inde_page,
            "to_inde_page_type": type(to_inde_page).__name__,
        },
    )
    
# templates下的index.html
<p>
		{{ to_inde_page }}: {{ to_inde_page_type }}
</p>

https://ithelp.ithome.com.tw/upload/images/20240916/20161866V5y0AEZUpi.png

可以看到可以匹配到更靈活的url

那如果想要匹配更加靈活的話,可以使用到re_path作為url的配置,也就是正規表達式。只是因為在一般狀況下path就能滿足大部分需求,因此這邊就不展開說明

想使用可以去看官方文檔:

https://docs.djangoproject.com/en/5.1/ref/urls/#django.urls.re_path

如何在Template與View中更簡潔的調用路由-路由的命名

如果網頁的規模提升,路由的數量一多起來,不但更難管理,程式碼的可讀性就會下降

藉由對路由進行命名,開發時更能了解這個路由具備什麼樣的功能

# 根目錄下的url.py
urlpatterns = [
    ...
    path("article/", include(("article.urls", "article"), namespace="article")),
		...
]
# 我們為article這個app下所有路由的空間命名為article(namespace)
# article.urls後面的article不能為空,通常直接設為app名稱,如果不寫會出現以下警告
django.core.exceptions.ImproperlyConfigured: Specifying a namespace in include() without providing an app_name is not supported. Set the app_name attribute in the included module, or pass a 2-tuple containing the list of patterns and app_name instead.
# article下的urls.py
urlpatterns = [
    ...
    path("tag/<int:tag_id>/", tag_detail_view, name="tag_detail_view"),
]
# 將路由命名為tag_tetail_view

在templates與views中使用命名的方式如下

# templates
<div>
		<a href="{% url "article:tag_detail_view" "1" %}">tag_detail_view</a>
		<p>the link of the above url: <strong>{% url "article:tag_detail_view" "1" %}</strong></p>
	</div>
# url內的規則為 <namespace>:<url_name>
# 如果有變數就是放後面,有多個的話就是如下
# "" "" ""

# views.py
from django.urls import reverse

def demo_view(request):
    url = reverse("article:tag_detail_view", args=[1])
    return render(request, "demo.html", {"url": url})

我們可以很輕易的判斷這會是在article app下的標籤詳情頁,因此增加程式碼的可讀性

如果團隊對於命名有一定規範的,應該更能體現這樣命名的優勢

並且也能透過resolve方法,來取得路由對象,獲取路由的詳細資訊

詳細可以看官方文檔:https://docs.djangoproject.com/en/5.1/ref/urlresolvers/#resolve

Template

Template中的內置標籤

在前面的案例中有一些模版的基本配置,這邊我們展開來繼續說

首先介紹Django內建的常用模板標籤

# HTML file
# 遍歷可迭代對象
{% for item in items %}
{{ item }}
{% endfor %}

# 條件判斷
{% if url %}
		<p>{{ url }}</p>
{% elif not url %}
		<p>url is not defined</p>
{% else %}		
{% endif %}

# 生成csrf_token標籤
{% csrf_token %}

# 生成對應的url
{% url "" %}

# 將變數重新命名
{% with %}

# 載入Django的標籤,不論是內建或是自定義還是第三方的
{% load xxx %} # {% load staic %} {% load staticfiles %}

# 讀取靜態檔案
{% static "css/style.css" %}

# 模板繼承
{% extends "base.html" %}

# 表示載入特定模板
{% include "card.html" %}

# 重寫父模板(繼承模板)中的特定區塊
{% block xxx %}
{% endblock xxx %}

讓Template的應用更上一層樓:extends、include與組件化

我們在前面的範例中,可以看到很多時候就是一個視圖搭配一個HTML,然後裡面的HTML又臭又長。簡單的規模是還好,但是如果網站規模一起來可想而知會有多難管理。

如果是有前端開發經驗的讀者,一定對於管理Component不陌生,雖然Django本身沒有類似State的管理機制,但是透過繼承、傳遞參數與條件判斷,也能做出類似props的效果,方便大量的模板

以下進入範例:

# templates下建立base.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Base</title>
	{% block extra_header %}
	<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
	{% endblock extra_header %}
</head>
<body>
	{% block content  %}{% endblock content  %}
	{% block extra_js %}
	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
	{% endblock extra_js %}
</body>
</html>

我們在基礎頁面中添加了Bootstrap的css與js,並且把他們包在特定的block中,當然如果整個站點的所有網站都會需要用到Bootstrap可以不用這樣做,只是單純增加彈性而已

如果想要改寫block內容的同時,又不會刪除原有的內容,可以這樣寫

{% block extra_header %}
{{ block.super }}

{% endblock %}

OK~我們繼續

# urls.py
urlpatterns = [
    ...
    path("demo_html_tag/", demo_html_tag_view),
]

# views.py
def demo_html_tag_view(request):
    samoyed_content = "The Samoyed is a breed of large herding dog that descended from the Nenets herding laika, a spitz-type dog, with a thick, white, double-layer coat."

    golden_retriever_content = "The Golden Retriever is a medium-large gun dog that was bred to retrieve shot waterfowl, such as ducks and upland game birds, during hunting and shooting parties."

    return render(
        request,
        "demo_html_tag.html",
        {
            "samoyed_content": samoyed_content,
            "golden_retriever_content": golden_retriever_content,
        },
    )

然後在static下建立images資料夾,並且放入我們等一下會用到的圖片,圖片都是用unsplash的免費素材

接著我們回到我們的主角templates們

# templates下建立demo_html_tag.html
{% extends "base.html" %}

{% block content %}
<section class="card_container container">
	<div class="row">
		<div class="col-lg-6">
			{% include "card.html" with title="Samoyed Dogs" content=samoyed_content img="images/samoyed.jpg" %}
		</div>
		<div class="col-lg-6">
			{% include "card.html" with title="Golden Retriever" content=golden_retriever_content img="images/golden.jpg" %}
		</div>
	</div>
</section>
{% endblock content %}

# templates下建立card.html
{% load static %}
<div class="card" style="width: 18rem;">
	{% if img %}
	<img src="{% static img %}" class="card-img-top" alt="...">
	{% else %}
	<img src="https://via.placeholder.com/150" class="card-img-top" alt="...">
	{% endif %}
	<div class="card-body">
		{% if title %}
	    <h5 class="card-title">{{ title }}</h5>
	    {% else %}
	  	<h5 class="card-title">Card title</h5>
	    {% endif %}

	  	{% if content %}		
	  	<p class="card-text">{{ content }}</p>
		{% else %}
		<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
		{% endif %}
		
	  	<a href="#" class="btn btn-primary">Go somewhere</a>
	</div>
  </div>

好~我們來解釋一下我們都做什麼

首先templates資料夾會是像是這樣
https://ithelp.ithome.com.tw/upload/images/20240916/201618667CfxD8Jbvp.png

base.html是大家的基礎模板,demo_html_tag.html則是要渲染的頁面,而card.html則是像元件一樣的角色

所以在demo_html_tag.html中,我們需要繼承父模板的資料,使用到extends標籤。接著我們想要客製化每一張card自己的內容,所以除了將card.html進行include之外,也進行了傳參

最後在card中,為了增加元件的可用性,會需要對變數的存在做基本的條件判斷

最後來到瀏覽器,就可以看到我們可愛的兩張卡片啦
https://ithelp.ithome.com.tw/upload/images/20240916/201618668Uo9HEzcyI.png

Django的模板透過繼承、包含與傳參等等作用,讓我們在做開發時,即使增加頁面的數量,也能在app下分別建立template方便管理大量的模板,並且也不需要很高的學習門檻就能看懂程式碼的運作

其他更多的模板標籤可以參考官方文檔:

https://docs.djangoproject.com/en/5.1/ref/templates/builtins/

並且也能夠自定義自己的標籤模板,過濾器等等,這邊也不一一說明了,有興趣的就來讀文檔吧XD

https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/

Model

在前面已經有稍微提過Model中可以設置不同的欄位類型,這邊就不再贅述

我們會針對幾個部分來進行介紹:

  • Model中還有哪些常用或是可以使用的配置
  • View中基礎的ORM語法

Model中欄位其他常用配置

class DemoModel(models.Model):
    name = models.CharField(max_length=120)
    age = models.IntegerField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_active = models.BooleanField(default=True)
    is_deleted = models.BooleanField(default=False)
    category = models.ForeignKey("Category", on_delete=models.CASCADE)
    author = models.ForeignKey("Author", on_delete=models.CASCADE)
    tags = models.ManyToManyField("Tag")

    class Meta:
        db_table = "demo_model" # 設置資料表名稱
        verbose_name = "Demo Model" # 設置在admin後台顯示的名稱
        verbose_name_plural = "Demo Model" # 設置在admin後台顯示的名稱(複數形式)
        ordering = ["-created_at"] # 設置資料排序方式 - 表示降序,默認表示升序
        abstract = True # 設置為抽象模型,不會生成資料表
        unique_together = ["name", "age"] # 設置唯一索引
        indexes = [models.Index(fields=["name", "age"])] # 設置索引
        permissions = [("can_read_demo_model", "Can read demo model")] # 設置權限
        app_label = "article" # 設置應用名稱 如果不設置則默認為應用名稱

    def __str__(self):
        return self.name # 返回模型實例的名稱
    
    def save(self, *args, **kwargs):
        # 可以自定義保存邏輯
        super().save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        # 可以自定義刪除邏輯
        super().delete(*args, **kwargs)

    def clean(self) -> None:
        # 可以自定義驗證邏輯 例如驗證name是否為空 通常用於表單驗證
        if not self.name:
            raise ValueError("name cannot be empty")
        return super().clean()
    
    def get_absolute_url(self):
        # 返回模型實例的絕對URL
        return f"/demo_model/{self.pk}/"

除此之外,也能夠過封裝的方法,來讓某些方法能夠被複用

class DemoManager(models.Manager):
    def is_active(self):
        return self.filter(is_active=True)

class DemoModel(models.Model):
    ...
    objects = DemoManager()

在上面的DemoManger中,我們通過定義is_active方法,來做到:

  • 提升程式碼的可讀性,
  • 可以統一邏輯,如果有多個Model實例需要複用相同方法時可以不用一直重寫,要修改也更加方便
Demo.objects.filter(is_active=True)
Demo.objects.is_active()

透過在Model中做好設定,可以在視圖函式的撰寫中減少許多重複的邏輯寫法

尤其是clean()與save()方法,在開發上增加了更多的靈活度

在後面介紹form跟admin的時候,會做更多的示範,到時後就能體會到Django在處理數據上在自由度與封裝度都給出了很多自由,讓開發者能更專注在功能開發上

只是要注意如果是使用批量插入數據時例如bulk_create()

不會觸發save方法,所以在使用一些Queryset API上時需要特別注意,詳情可以看官方文檔:

https://docs.djangoproject.com/en/4.2/ref/models/querysets/#bulk-create

View中的ORM語法

雖然Django中也能支援寫SQL原生語法(在一些很複雜的語句上),但是在大部分狀況下,ORM語法已經能支援大部分的開發情境,以下是一些基本的ORM操作

  1. 用不同方式取得對象
def article_view(request):
    articles = Article.objects.all()  # 取得所有文章
    article = Article.objects.get(article_id=1)  # 取得文章 id=1 的文章
    article, created = Article.objects.get_or_create(
        title="Hello", content="World"
    )  # 取得或新增文章
    articles = Article.objects.filter(title__contains="Hello")  # 篩選文章
    articles = Article.objects.exclude(title__contains="Hello")  # 排除文章
    articles = Article.objects.order_by("-created_at")  # 排序文章
    articles = Article.objects.order_by("created_at")[0:2]  # 取得前兩篇文章
    return render(request, "article.html", {"articles": articles})
  1. 儲存與更新對象
def create_or_update_article(request):
    if request.method == "POST":
        title = request.POST["title"]
        content = request.POST["content"]
        try:
            article = get_object_or_404(Article, title=title)
            article.content = request.POST["content"]
            article.save()
        except:
            article = Article.objects.create(title=title, content=content)
    return render(request, "article_detail.html", locals())
  1. 刪除對象
from django.shortcuts import render, get_object_or_404

def delete_article(request, pk):
    article = get_object_or_404(Article, pk=pk)
    article.delete()
    return redirect('article_list')
  1. 透過Q來做出比較複雜的查詢
from django.db.models import Q

def search_articles(request):
    article_query = request.GET.get("article_query")
    articles = Article.objects.filter(
        Q(title__contains=article_query) | Q(content__contains=article_query)
    )
  1. 針對多表關聯的處理
  • 設置related_name 能夠進行反向查詢
class Article(models.Model):
    ...
    category = models.ForeignKey("Category", on_delete=models.CASCADE, related_name='articles')
    author = models.ForeignKey("Author", on_delete=models.CASCADE, related_name='articles')
    tags = models.ManyToManyField("Tag", related_name='articles')
    
 # 獲取某個分類的所有文章
category = Category.objects.get(category_id=1)
articles_in_category = category.articles.all()

# 獲取某個作者的所有文章
author = Author.objects.get(author_id=1)
articles_by_author = author.articles.all()

# 獲取包含某個標籤的所有文章
tag = Tag.objects.get(tag_id=1)
articles_with_tag = tag.articles.all()
  • 使用select_related優化一對多查詢
# 預先加載 category 和 author,減少數據庫查詢
articles = Article.objects.select_related('category', 'author').all()

for article in articles:
    print(f"{article.title} - {article.category.name} by {article.author.name}")
  • 使用prefetch_related優化多對多查詢
# 預先加載 tags,減少數據庫查詢
articles = Article.objects.prefetch_related('tags').all()

for article in articles:
    print(f"{article.title} - Tags: {', '.join(tag.name for tag in article.tags.all())}")

上述例子展示了 Django ORM 在處理複雜關聯查詢時的強大功能,可以有效地進行數據檢索和操作,同時保持程式碼的簡潔和可讀性

今日總結

我們今天更加深入了解到Django MTV結構下,不同單元之間是怎麼協同合作的

  1. url中可以透過空間命名與路由命名來簡化template與view中調用路由的方式
  2. 透過繼承與傳遞參數的方式能夠更加自由的管理整個網站的模板
  3. 設置好Model搭配ORM語法能夠大幅簡化業務邏輯的程式碼量與增進可讀性

但是有關Model的部分還是有許多部分可以再深入的探討,明天我們會針對多資料庫,以及如何動態的添加模型做更深入的探討


上一篇
Django in 2024: 總是得從這裡開始,Hello world!第一個Django專案
下一篇
Django in 2024: 徹底玩轉Model,多資料庫開發與動態添加表格功能
系列文
Django 2024: 從入門到SaaS實戰16
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言